﻿// -----------------------------------------------------------------------
// <copyright file="TypeEx.cs" company="none">
// Copyright (c) 2012 M. Alberti, xanatos(at)live.it
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.
// </copyright>
// -----------------------------------------------------------------------

namespace FluentStatement.Utilities
{
    using System;
    using System.Collections.Concurrent;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Runtime.CompilerServices;

    /// <summary>
    /// 
    /// </summary>
    public static class TypeEx
    {
        /// <summary>
        /// Type, Tuple&lt;Methods of type, (for interfaces)methods of base interfaces shadowed&gt;.
        /// </summary>
        public static readonly ConcurrentDictionary<Type, Tuple<MethodInfo[], HashSet<MethodInfo>>> Methods = new ConcurrentDictionary<Type, Tuple<MethodInfo[], HashSet<MethodInfo>>>();

        /// <summary>
        /// Type, Tuple&lt;Properties of type, (for interfaces)properties of base interfaces shadowed&gt;.
        /// </summary>
        public static readonly ConcurrentDictionary<Type, Tuple<PropertyInfo[], HashSet<PropertyInfo>>> Properties = new ConcurrentDictionary<Type, Tuple<PropertyInfo[], HashSet<PropertyInfo>>>();

        /// <summary>
        /// This method can even be used for searching for generic extension methods (IEnumerable&lt;T&gt;).
        /// You simply need to pass to <paramref name="type"/> typeof(IEnumerable&lt;&gt;).
        /// </summary>
        /// <param name="method"></param>
        /// <param name="type"></param>
        /// <returns></returns>
        public static bool IsExtensionMethodOf(this MethodInfo method, Type type)
        {
            // We cheat a little and don't check if the containing class is marked by the ExtensionAttribute.
            if (method.GetCustomAttributes(typeof(ExtensionAttribute), false).Length == 0)
            {
                return false;
            }

            ParameterInfo[] pars = method.GetParameters();

            if (pars.Length == 0)
            {
                return false;
            }

            Type par = pars[0].ParameterType;

            if (type.IsGenericTypeDefinition)
            {
                if (!par.IsGenericType)
                {
                    return false;
                }

                par = par.GetGenericTypeDefinition();
            }

            if (par != type)
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static bool IsNullable(this Type type)
        {
            return type.IsValueType && type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        /// <remarks>Based on code from http://www.liensberger.it/web/blog/?p=191 .</remarks>
        public static bool IsAnonymousType(this Type type)
        {
            // HACK: The only way to detect anonymous types right now.
            return Attribute.IsDefined(type, typeof(CompilerGeneratedAttribute), false)
                && type.IsGenericType && type.Name.Contains("AnonymousType")
                && (type.Name.StartsWith("<>") || type.Name.StartsWith("VB$"))
                && (type.Attributes & TypeAttributes.NotPublic) == TypeAttributes.NotPublic;
        }

        /// <summary>
        /// Returns the interfaces of a type. <paramref name="interface"/> can be an open generic (e.g. IEnumerable&lt;&gt;).
        /// </summary>
        /// <param name="type"></param>
        /// <param name="interface"></param>
        /// <returns></returns>
        public static Type[] GetInterfaces(this Type type, Type @interface)
        {
            if (!@interface.IsInterface)
            {
                throw new ArgumentException("@interface");
            }

            if (type == @interface)
            {
                return new[] { type };
            }

            if (!@interface.IsGenericTypeDefinition)
            {
                return type.GetInterfaces().Where(p => p == @interface).ToArray();
            }

            if (type.IsInterface && type.IsGenericType && type.GetGenericTypeDefinition() == @interface)
            {
                return new[] { type };
            }

            // @interface is an open generic
            Type[] interfaces = type.GetInterfaces().Where(p => p.IsGenericType && p.GetGenericTypeDefinition() == @interface).ToArray();

            return interfaces;
        }

        /// <summary>
        /// Like Type.IsAssignableFrom but works with open types (e.g. IEnumerable&lt;&gt;, List&lt;&gt;, Dictionary&lt;,&gt;...), classes and interfaces.
        /// </summary>
        /// <param name="type"></param>
        /// <param name="type2"></param>
        /// <returns></returns>
        public static bool InheritsOrImplements(this Type type, Type type2)
        {
            if (type == type2)
            {
                return true;
            }

            if (!type.IsGenericTypeDefinition)
            {
                return type.IsAssignableFrom(type2);
            }

            if (type.IsInterface)
            {
                return type2.GetInterfaces().Any(p => p.GetGenericTypeDefinition() == type);
            }

            if (type2.IsInterface)
            {
                return false;
            }

            while (type2.IsGenericType)
            {
                type2 = type2.GetGenericTypeDefinition();

                if (type.IsAssignableFrom(type2))
                {
                    return true;
                }

                type2 = type2.BaseType;
            }

            return false;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <param name="c"></param>
        /// <returns></returns>
        public static bool IsSimplyAssignableFrom(this Type type, Type c)
        {
            if (type.IsValueType || c.IsValueType)
            {
                return type == c;
            }

            return type.IsAssignableFrom(c);
        }

        /// <summary>
        /// Returns the public, visible (non-overridden/non-hidden) methods of a type.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static MethodInfo[] GetVisibleMethods(this Type type)
        {
            if (type.IsInterface)
            {
                return (MethodInfo[])type.GetVisibleMethodsInterfaceImpl().Item1.Clone();
            }

            return (MethodInfo[])type.GetVisibleMethodsImpl().Clone();
        }

        /// <summary>
        /// Returns the public, visible (non-overridden/non-hidden) properties of a type.
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        public static PropertyInfo[] GetVisibleProperties(this Type type)
        {
            if (type.IsInterface)
            {
                return (PropertyInfo[])type.GetVisiblePropertiesInterfaceImpl().Item1.Clone();
            }

            return (PropertyInfo[])type.GetVisiblePropertiesImpl().Clone();
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static MethodInfo[] GetVisibleMethodsImpl(this Type type)
        {
            Tuple<MethodInfo[], HashSet<MethodInfo>> tuple;

            if (Methods.TryGetValue(type, out tuple))
            {
                return tuple.Item1;
            }

            MethodInfo[] methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public);

            if (type.BaseType == null)
            {
                Methods.TryAdd(type, Tuple.Create(methods, (HashSet<MethodInfo>)null));
                return methods;
            }

            List<MethodInfo> baseMethods = type.BaseType.GetVisibleMethodsImpl().ToList();

            foreach (var method in methods)
            {
                if (method.IsHideByName())
                {
                    baseMethods.RemoveAll(p => p.Name == method.Name);
                }
                else
                {
                    int numGenericArguments = method.GetGenericArguments().Length;
                    ParameterInfo[] parameters = method.GetParameters();

                    baseMethods.RemoveAll(p =>
                    {
                        if (!method.EqualSignature(numGenericArguments, parameters, p))
                        {
                            return false;
                        }

                        return true;
                    });
                }
            }

            if (baseMethods.Count == 0)
            {
                Methods.TryAdd(type, Tuple.Create(methods, (HashSet<MethodInfo>)null));
                return methods;
            }

            var methods3 = new MethodInfo[methods.Length + baseMethods.Count];

            Array.Copy(methods, 0, methods3, 0, methods.Length);
            baseMethods.CopyTo(methods3, methods.Length);

            Methods.TryAdd(type, Tuple.Create(methods3, (HashSet<MethodInfo>)null));
            return methods3;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static Tuple<MethodInfo[], HashSet<MethodInfo>> GetVisibleMethodsInterfaceImpl(this Type type)
        {
            Tuple<MethodInfo[], HashSet<MethodInfo>> tuple;

            if (Methods.TryGetValue(type, out tuple))
            {
                return tuple;
            }

            MethodInfo[] methods = type.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public);

            Type[] baseInterfaces = type.GetInterfaces();

            if (baseInterfaces.Length == 0)
            {
                tuple = Tuple.Create(methods, new HashSet<MethodInfo>());
                Methods.TryAdd(type, tuple);
                return tuple;
            }

            var baseMethods = new List<MethodInfo>();

            var baseMethodsTemp = new MethodInfo[baseInterfaces.Length][];

            var shadowedMethods = new HashSet<MethodInfo>();

            for (int i = 0; i < baseInterfaces.Length; i++)
            {
                var tuple2 = baseInterfaces[i].GetVisibleMethodsInterfaceImpl();
                baseMethodsTemp[i] = tuple2.Item1;
                shadowedMethods.UnionWith(tuple2.Item2);
            }

            for (int i = 0; i < baseInterfaces.Length; i++)
            {
                baseMethods.AddRange(baseMethodsTemp[i].Where(p => !shadowedMethods.Contains(p)));
            }

            foreach (var method in methods)
            {
                if (method.IsHideByName())
                {
                    baseMethods.RemoveAll(p =>
                    {
                        if (p.Name == method.Name)
                        {
                            shadowedMethods.Add(p);
                            return true;
                        }

                        return false;
                    });
                }
                else
                {
                    int numGenericArguments = method.GetGenericArguments().Length;
                    ParameterInfo[] parameters = method.GetParameters();

                    baseMethods.RemoveAll(p =>
                    {
                        if (!method.EqualSignature(numGenericArguments, parameters, p))
                        {
                            return false;
                        }

                        shadowedMethods.Add(p);
                        return true;
                    });
                }
            }

            if (baseMethods.Count == 0)
            {
                tuple = Tuple.Create(methods, shadowedMethods);
                Methods.TryAdd(type, tuple);
                return tuple;
            }

            var methods3 = new MethodInfo[methods.Length + baseMethods.Count];

            Array.Copy(methods, 0, methods3, 0, methods.Length);
            baseMethods.CopyTo(methods3, methods.Length);

            tuple = Tuple.Create(methods3, shadowedMethods);
            Methods.TryAdd(type, tuple);
            return tuple;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static PropertyInfo[] GetVisiblePropertiesImpl(this Type type)
        {
            Tuple<PropertyInfo[], HashSet<PropertyInfo>> tuple;

            if (Properties.TryGetValue(type, out tuple))
            {
                return tuple.Item1;
            }

            PropertyInfo[] properties = type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public);

            if (type.BaseType == null)
            {
                Properties.TryAdd(type, Tuple.Create(properties, (HashSet<PropertyInfo>)null));
                return properties;
            }

            List<PropertyInfo> baseProperties = type.BaseType.GetVisiblePropertiesImpl().ToList();

            foreach (var property in properties)
            {
                if (property.IsHideByName())
                {
                    baseProperties.RemoveAll(p => p.Name == property.Name);
                }
                else
                {
                    ParameterInfo[] indexers = property.GetIndexParameters();

                    baseProperties.RemoveAll(p =>
                    {
                        if (!property.EqualSignature(indexers, p))
                        {
                            return false;
                        }

                        return true;
                    });
                }
            }

            if (baseProperties.Count == 0)
            {
                Properties.TryAdd(type, Tuple.Create(properties, (HashSet<PropertyInfo>)null));
                return properties;
            }

            var properties3 = new PropertyInfo[properties.Length + baseProperties.Count];

            Array.Copy(properties, 0, properties3, 0, properties.Length);
            baseProperties.CopyTo(properties3, properties.Length);

            Properties.TryAdd(type, Tuple.Create(properties3, (HashSet<PropertyInfo>)null));
            return properties3;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private static Tuple<PropertyInfo[], HashSet<PropertyInfo>> GetVisiblePropertiesInterfaceImpl(this Type type)
        {
            Tuple<PropertyInfo[], HashSet<PropertyInfo>> tuple;

            if (Properties.TryGetValue(type, out tuple))
            {
                return tuple;
            }

            PropertyInfo[] properties = type.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public);

            Type[] baseInterfaces = type.GetInterfaces();

            if (baseInterfaces.Length == 0)
            {
                tuple = Tuple.Create(properties, new HashSet<PropertyInfo>());
                Properties.TryAdd(type, tuple);
                return tuple;
            }

            var baseProperties = new List<PropertyInfo>();

            var basePropertiesTemp = new PropertyInfo[baseInterfaces.Length][];

            var shadowedProperties = new HashSet<PropertyInfo>();

            for (int i = 0; i < baseInterfaces.Length; i++)
            {
                var tuple2 = baseInterfaces[i].GetVisiblePropertiesInterfaceImpl();
                basePropertiesTemp[i] = tuple2.Item1;
                shadowedProperties.UnionWith(tuple2.Item2);
            }

            for (int i = 0; i < baseInterfaces.Length; i++)
            {
                baseProperties.AddRange(basePropertiesTemp[i].Where(p => !shadowedProperties.Contains(p)));
            }

            foreach (var property in properties)
            {
                if (property.IsHideByName())
                {
                    baseProperties.RemoveAll(p =>
                    {
                        if (p.Name == property.Name)
                        {
                            shadowedProperties.Add(p);
                            return true;
                        }

                        return false;
                    });
                }
                else
                {
                    ParameterInfo[] indexers = property.GetIndexParameters();

                    baseProperties.RemoveAll(p =>
                    {
                        if (!property.EqualSignature(indexers, p))
                        {
                            return false;
                        }

                        shadowedProperties.Add(p);
                        return true;
                    });
                }
            }

            if (baseProperties.Count == 0)
            {
                tuple = Tuple.Create(properties, shadowedProperties);
                Properties.TryAdd(type, tuple);
                return tuple;
            }

            var properties3 = new PropertyInfo[properties.Length + baseProperties.Count];

            Array.Copy(properties, 0, properties3, 0, properties.Length);
            baseProperties.CopyTo(properties3, properties.Length);

            tuple = Tuple.Create(properties3, shadowedProperties);
            Properties.TryAdd(type, tuple);
            return tuple;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="method1"></param>
        /// <param name="numGenericArguments1"></param>
        /// <param name="parameters1"></param>
        /// <param name="method2"></param>
        /// <returns></returns>
        private static bool EqualSignature(this MethodInfo method1, int numGenericArguments1, ParameterInfo[] parameters1, MethodInfo method2)
        {
            // To shadow by signature a method must have same name, same number of 
            // generic arguments, same number of parameters and same parameters' type
            if (method1.Name != method2.Name)
            {
                return false;
            }

            if (numGenericArguments1 != method2.GetGenericArguments().Length)
            {
                return false;
            }

            ParameterInfo[] parameters2 = method2.GetParameters();

            if (!parameters1.EqualParameterTypes(parameters2))
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="property1"></param>
        /// <param name="indexers1"></param>
        /// <param name="property2"></param>
        /// <returns></returns>
        private static bool EqualSignature(this PropertyInfo property1, ParameterInfo[] indexers1, PropertyInfo property2)
        {
            // To shadow by signature a property must have same name, 
            // same number of indexers and same indexers' type
            if (property1.Name != property2.Name)
            {
                return false;
            }

            ParameterInfo[] parameters2 = property1.GetIndexParameters();

            if (!indexers1.EqualParameterTypes(parameters2))
            {
                return false;
            }

            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="parameters1"></param>
        /// <param name="parameters2"></param>
        /// <returns></returns>
        private static bool EqualParameterTypes(this ParameterInfo[] parameters1, ParameterInfo[] parameters2)
        {
            if (parameters1.Length != parameters2.Length)
            {
                return false;
            }

            for (int i = 0; i < parameters1.Length; i++)
            {
                if (parameters1[i].IsOut != parameters2[i].IsOut)
                {
                    return false;
                }

                if (parameters1[i].ParameterType.IsGenericParameter)
                {
                    if (!parameters2[i].ParameterType.IsGenericParameter)
                    {
                        return false;
                    }

                    if (parameters1[i].ParameterType.GenericParameterPosition != parameters2[i].ParameterType.GenericParameterPosition)
                    {
                        return false;
                    }
                }
                else if (parameters1[i].ParameterType != parameters2[i].ParameterType)
                {
                    return false;
                }
            }

            return true;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="method"></param>
        /// <returns></returns>
        private static bool IsHideByName(this MethodInfo method)
        {
            if (!method.Attributes.HasFlag(MethodAttributes.HideBySig) && (!method.Attributes.HasFlag(MethodAttributes.Virtual) || method.Attributes.HasFlag(MethodAttributes.NewSlot)))
            {
                return true;
            }

            return false;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="property"></param>
        /// <returns></returns>
        private static bool IsHideByName(this PropertyInfo property)
        {
            MethodInfo get = property.GetGetMethod(false);

            if (get != null && get.IsHideByName())
            {
                return true;
            }

            MethodInfo set = property.GetSetMethod(false);

            if (set != null && set.IsHideByName())
            {
                return true;
            }

            return false;
        }
    }
}